Skip to content

ci: automate semver releases on main using Sonnet#162

Merged
benvinegar merged 4 commits intomainfrom
ci/release-on-main-sonnet
Feb 24, 2026
Merged

ci: automate semver releases on main using Sonnet#162
benvinegar merged 4 commits intomainfrom
ci/release-on-main-sonnet

Conversation

@benvinegar
Copy link
Member

Summary

Adds a GitHub Actions workflow to automate release decisions on pushes to main using Claude Sonnet.

What it does

  • Runs on push to main and workflow_dispatch
  • Debounces rapid pushes via concurrency cancellation
  • Analyzes merged PRs since latest tag
  • Uses CI_ANTHROPIC_KEY + Sonnet to decide none/patch/minor
  • Blocks major bumps in CI
  • Bumps package.json version (+ lockfile if present)
  • Commits, tags, pushes, and creates GitHub Release with changelog
  • Supports [skip release] opt-out and idempotency checks

Notes

Uses GITHUB_TOKEN with minimal permissions (contents: write, pull-requests: read).

@greptile-apps
Copy link

greptile-apps bot commented Feb 24, 2026

Greptile Summary

Introduces automated semantic versioning release workflow triggered on pushes to main, using Claude Sonnet to analyze merged PRs and decide between none/patch/minor version bumps.

Critical Issues Found:

  • Infinite loop: Pushing release commit to main (line 210) re-triggers this workflow without [skip ci] in commit message
  • Race condition: Concurrent workflow runs can both pass tag existence check, then conflict when pushing (lines 194-211)
  • Idempotency broken: Tag existence check (line 194) exits successfully but doesn't prevent subsequent commit/push steps
  • Shell injection: Version bump logic (lines 180-181) interpolates shell variables into JavaScript, vulnerable if version strings contain special characters

Other Observations:

  • Proper use of contents: write permission scope
  • Good concurrency cancellation to debounce rapid pushes
  • Claude API integration uses temperature 0 for deterministic decisions
  • [skip release] opt-out mechanism works correctly

Confidence Score: 1/5

  • This PR has critical bugs that will cause runtime failures and should not be merged without fixes
  • Score reflects multiple severe logical errors: infinite workflow loop, race conditions in concurrent releases, broken idempotency check, and shell injection vulnerabilities in version bumping code
  • .github/workflows/release-on-main.yml requires immediate attention — all identified issues must be resolved before merge

Important Files Changed

Filename Overview
.github/workflows/release-on-main.yml Automated semver release workflow with Claude Sonnet — contains critical race conditions, infinite loop risk, and shell injection vulnerabilities

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Push to main] --> B{Contains skip release?}
    B -->|Yes| Z[Skip workflow]
    B -->|No| C[Fetch tags and PRs]
    C --> D{Any merged PRs?}
    D -->|No| E[Exit: No changes]
    D -->|Yes| F[Call Claude Sonnet API]
    F --> G{Decision?}
    G -->|none| E
    G -->|patch or minor| H[Bump package.json version]
    H --> I{Tag exists?}
    I -->|Yes| J[Exit 0 but steps continue]
    I -->|No| K[Commit Tag Push]
    K --> L[Push triggers workflow again]
    J --> M[Attempt duplicate commit]
    K --> N[Create GitHub Release]
    N --> O[Done]
    
    style L fill:#ff6b6b
    style M fill:#ff6b6b
    style J fill:#ffd93d
Loading

Last reviewed commit: 08bb666

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 6 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 194 to 197
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Tag already exists: $TAG. Skipping to keep idempotent."
exit 0
fi
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tag existence check doesn't prevent subsequent steps. If tag exists, this step exits 0 (success) but next steps still execute, causing duplicate commit attempts.

Suggested change
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Tag already exists: $TAG. Skipping to keep idempotent."
exit 0
fi
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Tag already exists: $TAG. Skipping to keep idempotent." >> "$GITHUB_STEP_SUMMARY"
echo "skip_remaining=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "skip_remaining=false" >> "$GITHUB_OUTPUT"
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/release-on-main.yml
Line: 194-197

Comment:
Tag existence check doesn't prevent subsequent steps. If tag exists, this step exits 0 (success) but next steps still execute, causing duplicate commit attempts.

```suggestion
          if git rev-parse "$TAG" >/dev/null 2>&1; then
            echo "Tag already exists: $TAG. Skipping to keep idempotent." >> "$GITHUB_STEP_SUMMARY"
            echo "skip_remaining=true" >> "$GITHUB_OUTPUT"
            exit 0
          fi
          echo "skip_remaining=false" >> "$GITHUB_OUTPUT"
```

How can I resolve this? If you propose a fix, please make it concise.

fi

- name: Commit and tag
if: steps.bump.outputs.tag != ''
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also needs to check steps.tag_check.outputs.skip_remaining != 'true' to prevent commit when tag already exists

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/release-on-main.yml
Line: 200

Comment:
Also needs to check `steps.tag_check.outputs.skip_remaining != 'true'` to prevent commit when tag already exists

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 207 to 211
git add package.json package-lock.json || true
git commit -m "release: ${TAG}"
git tag "$TAG"
git push origin main
git push origin "$TAG"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition: if multiple workflow runs create same version, both pass line 194 check, then both commit/push, causing git conflict. Second push will fail with non-fast-forward error.

Use atomic git push --atomic or add retry logic with conflict detection.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/release-on-main.yml
Line: 207-211

Comment:
Race condition: if multiple workflow runs create same version, both pass line 194 check, then both commit/push, causing git conflict. Second push will fail with non-fast-forward error.

Use atomic `git push --atomic` or add retry logic with conflict detection.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — replaced the two separate git push calls with git push --atomic origin main "$TAG", ensuring commit+tag land together or not at all. Also added [skip release] to the release commit message (see next comment) so the concurrency window is further narrowed.

Responded by pi using anthropic/claude-sonnet-4-20250514.

Comment on lines 210 to 211
git push origin main
git push origin "$TAG"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushing back to main triggers this workflow again due to line 4-5. Creates infinite loop unless commit message includes [skip release] or [skip ci].

Add [skip ci] to commit message to prevent re-triggering.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/release-on-main.yml
Line: 210-211

Comment:
Pushing back to `main` triggers this workflow again due to line 4-5. Creates infinite loop unless commit message includes `[skip release]` or `[skip ci]`.

Add `[skip ci]` to commit message to prevent re-triggering.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed. The release commit message now includes [skip release] (e.g. release: v1.2.3 [skip release]), which is already checked by the job-level if condition on line 18. This prevents the infinite loop without needing [skip ci], which would also suppress other workflows we may want to run on release commits.

Responded by pi using anthropic/claude-sonnet-4-20250514.

run: |
set -euo pipefail
CURRENT=$(node -p "require('./package.json').version")
NEXT=$(node -e "const v='${CURRENT}'.split('.').map(Number); if('${BUMP}'==='minor'){v[1]++; v[2]=0;} else {v[2]++;} process.stdout.write(v.join('.')); ")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline JavaScript uses shell variable interpolation which breaks if CURRENT or BUMP contain special chars (quotes, backslashes). Use -- separator and stdin instead:

Suggested change
NEXT=$(node -e "const v='${CURRENT}'.split('.').map(Number); if('${BUMP}'==='minor'){v[1]++; v[2]=0;} else {v[2]++;} process.stdout.write(v.join('.')); ")
NEXT=$(node -e "const v=process.argv[1].split('.').map(Number); if(process.argv[2]==='minor'){v[1]++; v[2]=0;} else {v[2]++;} process.stdout.write(v.join('.'))" -- "${CURRENT}" "${BUMP}")
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/release-on-main.yml
Line: 180

Comment:
Inline JavaScript uses shell variable interpolation which breaks if `CURRENT` or `BUMP` contain special chars (quotes, backslashes). Use `--` separator and stdin instead:

```suggestion
          NEXT=$(node -e "const v=process.argv[1].split('.').map(Number); if(process.argv[2]==='minor'){v[1]++; v[2]=0;} else {v[2]++;} process.stdout.write(v.join('.'))" -- "${CURRENT}" "${BUMP}")
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied — both Node one-liners now use process.argv with -- separator instead of shell interpolation:

NEXT=$(node -e "const v=process.argv[1].split('.').map(Number); ..." -- "${CURRENT}" "${BUMP}")
node -e "... j.version=process.argv[1]; ..." -- "${NEXT}"

Eliminates the shell injection vector entirely.

Responded by pi using anthropic/claude-sonnet-4-20250514.

set -euo pipefail
CURRENT=$(node -p "require('./package.json').version")
NEXT=$(node -e "const v='${CURRENT}'.split('.').map(Number); if('${BUMP}'==='minor'){v[1]++; v[2]=0;} else {v[2]++;} process.stdout.write(v.join('.')); ")
node -e "const fs=require('fs'); const p='package.json'; const j=JSON.parse(fs.readFileSync(p,'utf8')); j.version='${NEXT}'; fs.writeFileSync(p, JSON.stringify(j,null,2)+'\\n');"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same shell injection risk with NEXT variable.

Suggested change
node -e "const fs=require('fs'); const p='package.json'; const j=JSON.parse(fs.readFileSync(p,'utf8')); j.version='${NEXT}'; fs.writeFileSync(p, JSON.stringify(j,null,2)+'\\n');"
node -e "const fs=require('fs'); const p='package.json'; const j=JSON.parse(fs.readFileSync(p,'utf8')); j.version=process.argv[1]; fs.writeFileSync(p, JSON.stringify(j,null,2)+'\\n');" -- "${NEXT}"
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/release-on-main.yml
Line: 181

Comment:
Same shell injection risk with `NEXT` variable.

```suggestion
          node -e "const fs=require('fs'); const p='package.json'; const j=JSON.parse(fs.readFileSync(p,'utf8')); j.version=process.argv[1]; fs.writeFileSync(p, JSON.stringify(j,null,2)+'\\n');" -- "${NEXT}"
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in the same commit — the package.json write now uses process.argv[1] with -- separator as suggested.

Responded by pi using anthropic/claude-sonnet-4-20250514.

@benvinegar benvinegar merged commit e50fcec into main Feb 24, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant